package lispinterpreter;

import java.util.ArrayList;
import java.util.HashMap;

/**
 * Clase principal que permite evaluar una instrucción Lisp para un subconjunto
 * sencillo de instrucciones para el dialecto Common LISP.
 * 
 * @author setian
 */
public class BlackBox {

    /**
     * Indicadores del as funciones elementales las cuales se usan para
     * identificar (o diferenciar) si un átomo que es de tipo string corresponde
     * a una variable o a una operación.
     */
    private String operators = "+ - * / < > = eq <= >=";

    /**
     * Contiene información de las variables y funciones definidas por el
     * usuario. Contiene la etiqueta de la función o variable y la lista
     * LispList asociada a cada una de ellas. Hasmap con la información de las
     * variables, su nombre y su lista LispList asociada así como el nombre y
     * cuerpo de las funciones definidas por el usuario
     */
    private HashMap<String, LispList> mapVariables = new HashMap<String, LispList>();
    
    
    private HashMap<String, String> mapFunctionVariables = new HashMap<String, String>();

    /**
     * Se encarga de clasificar y evaluar las operaciones de la instrucción
     * lisp. Clasifica por medio del operando de la instrucción el tipo de
     * acción que debe ocurrir.
     * 
     * @param Instrucción de Lisp en forma de estructura interana de de datos.
     * @return Lista LispList tras pasar por el proceso de evaluación.
     */
    public LispList evaluate(LispList ins) {

	// Obtiene el operando definido en la instrucción ins.
	String op = ((LispAtom) ins.getElement(0)).satom;

	if (op.equals("setf")) {
	    ins = setf(ins);
	} else if (op.equals("if")) {
	    ins = cond(ins);
	} else if (op.equals("defun")) {
	    ins = defun(ins);
	} else if (operators.contains(op)) {
	    // Reemplaza las variables de la instrucción por su valor.
	    ins = substituteVariables(ins);

	    // efectúa las funciones elementales.
	    ins = elemnetaryOperations(ins);

	} else if (!op.isEmpty() && !operators.contains(op)) {

	    if (ins.size() == 2) { // Es una función.
		ins = evaluateUserDefinedFunction(ins);
		evaluate(ins);
	    } else { // Es un átomo.
		ins = getValuesOf(op);
	    }
	} else
	    System.err.println("Undefined function " + op);

	return ins;
    }

    /**
     * ARIT. Efectúa las funciones elementales (+ - *) en la instrucción Lisp.
     * El método llama a la clase ElementaryFunctions la cuál efecúta las a
     * partir llamada recursiva de listas simples (sólo contiene átomos).
     */
    private LispList elemnetaryOperations(LispList ins) {
	// TODO Auto-generated method stub

	System.out.print("\n>> eval with elementary operations: ");
	ins.printLispList();

	LispList result = evaluateElementaryComplexList(ins);

	if (result.sAtomVerifier()) {
	    System.out.println("\n>> parcial eval: "
		    + ((LispAtom) result).fatom);
	} else if (result.bAtomVerifier()) {
	    System.out.println("\n>> parcial eval: "
		    + ((LispAtom) result).batom);
	} else if (result.fAtomVerifier()) {
	    System.out.println("\n>> parcial eval: "
		    + ((LispAtom) result).fatom);
	}
	return result;
    }

    /**
     * IF. Simple Conditional Evaluation provides three syntactic constructs for
     * conditional evaluation. if is the normal if-then-else expression (with an
     * optional else branch), cond is a conditional expression with multiple
     * branches and case branches if an expression has one of a set of constant
     * values.
     * 
     * @param ins
     * @return
     */
    private LispList cond(LispList ins) {
	// TODO Auto-generated method stub
	if (ins.size() != 4)
	    System.err.println("Too few parameters for special operator IF.");
	else {

	    LispList condition = ins.getElement(1);	// condición
	    LispList truePart = ins.getElement(2);	// LispList verdadera
	    LispList falsePart = ins.getElement(3);	// LispList falsa

	    boolean test = ((LispAtom) evaluate(condition)).batom;
	    
	    ins = (test) ? evaluate(truePart) : evaluate(falsePart);
	}
	return ins;
    }

    /**
     * SETF. The setf special form uses its first argument to define a place in
     * memory, evaluates its second argument, and stores the resulting value in
     * the resulting memory location.
     */
    private LispList setf(LispList ins) {
	int indexOperands = 0;

	if (ins.size() == 3) // (setf x 10)
	    indexOperands = 2;
	else if (ins.size() == 4) // (setf x '(1 2 3))
	    indexOperands = 3;
	else { // Instrucción no válida
	    System.err.println("SETF called with an odd number of arguments.");
	}

	// Nombre de la variable contendida en el átmo transformada a
	// mayúsculas.
	String variable = ((LispAtom) ins.getElement(1)).satom.toUpperCase();
	LispList operands = ins.getElement(indexOperands);

	// Agrega la variable y su definición a Hashmap
	mapVariables.put(variable, operands);
	return operands;
    }

    /**
     * (DEFUN name (-args-) -body-): Define function <name> with the given
     * bound-variable arguments <args>. When the function is called, the
     * arguments passed to it are evaluated and the <args> are bound to them.
     * Then the <body> is evaluated. When the function returns, the bound
     * variables are discarded and the value of the <body> is return.
     * 
     * @param ins
     * @return
     */
    private LispList defun(LispList ins) {
	String funcionName = null; // Nombre de la función.
	String variableName = null;
	LispList functionBody = null; // Cuerpo de la función.

	if (ins.size() == 4) // Definición de funciones.
	{
	    funcionName = ((LispAtom) ins.getElement(1)).satom.toUpperCase();
	    variableName = ((LispAtom) ins.getElement(2).getFirstElement()).satom.toUpperCase();
	    
	    functionBody = ins.getElement(3); //

	    mapVariables.put(funcionName, functionBody);
	    mapFunctionVariables.put(funcionName, variableName);
	    return functionBody;

	} 
	return functionBody;
    }

    private LispList evaluateUserDefinedFunction(LispList ins) {
	String funcionName = null; // Nombre de la función.
	String variableName = null;
	LispList functionBody = null; // Cuerpo de la función.
	funcionName = ((LispAtom) ins.getElement(0)).satom.toUpperCase();

	// XXX Mejorar la implementación las siguientes instrucciones cuyo
	// unico trabajo es objtener el valor del átomo.
	LispList value = ins.getElement(1);
	String atomValue = Float.toString(((LispAtom) value).fatom);
	variableName = mapFunctionVariables.get(funcionName);

	evaluate(LispParser.finalParse(LispParser.initialParse("(setf "
		+ variableName + " " + atomValue)));

	functionBody = getValuesOf(funcionName);
	functionBody = substituteVariables(functionBody);
	return functionBody;
    }
    
    /**
     * Sustituye las variables de la instrucción lisp por medio de un
     * implementación recursiva.
     * 
     * @return lista LispList con las variables sustituidas.
     */
    private LispList substituteVariables(LispList ins) {
	int len = ins.size();
	for (int i = 0; i < len; i++) {
	    LispList currentElement = ins.getElement(i);

	    // Verifica si el elemento corresponde a un atomo tipo string,
	    // float o boolean. Si el tipo no corresponde, es porque se tiene
	    // una lista anidada la cual es enviada nuevamente a este método
	    // para su análisis.
	    if (currentElement.atomVerifier()) {

		LispAtom atom = (LispAtom) currentElement;
		String expr = atom.satom;

		// Verifica si el operando corresponde a una funcion elemental.
		// Si no corresponde es por es una variable.
		if (expr != null && !operators.contains(expr)) {
		    LispList values = getValuesOf(expr.toUpperCase());
		    if (values == null) {
			System.err.print("Variable " + expr + " has no value.");
			break;
		    } else
			ins.replaceElement(i, values); // Reemplaza la variable
						       // por su valor.
		}
	    } else { // element es lista anidada.
		substituteVariables(currentElement);
	    }
	}
	return ins;
    }

    /**
     * Obtiene la definición de la variable al comparar el nombre de la variable
     * en el Hashmap de definiciones.
     * 
     * @param nombre de la variable de la cual se deben recuperar su definción.
     * @return definición de la variable si la encuentra o null.
     */
    private LispList getValuesOf(String variableName) {
	LispList value = mapVariables.get(variableName);
	return value;
    }
    
    
    
    // ===========================================================================
    //
    // ===========================================================================

    /**
     * @param complexList
     * @return
     */
    public static Object[] findFirstSimpleSublist(LispList complexList) {
	ArrayList<Integer> coordinatePile = new ArrayList<Integer>();
	boolean isSimple = complexList.isSimple();
	LispList currentList = complexList;
	if (isSimple) {
	    coordinatePile.add(-1);
	} else {
	    while (!isSimple) {
		for (int i = 0; i < currentList.getSize(); i++) {
		    if (!currentList.getElement(i).isAtom()) {
			currentList = currentList.getElement(i);
			coordinatePile.add(i);
			break;
		    }
		}
		isSimple = currentList.isSimple();
	    }
	}
	return coordinatePile.toArray();
    }

    /**
     * @param complexList
     * @param coordinates This parameter must be tyep int[] not Objetct. it is
     *        parsed within the method, each object to Integer. No exception is
     *        throwed because the method that used this method will always hand
     *        int[].
     * @return LispList with one simple sublist evaluated.
     */
    private LispList evaluateFirstSimpleSublist(LispList complexList,
	    int[] coordinates) {
	LispList toEvaluate = complexList;
	LispList newComplexList;
	LispAtom value;
	for (int i = 0; i < coordinates.length; i++) {
	    toEvaluate = toEvaluate.getElement(coordinates[i]);
	}
	value = (LispAtom) ElementaryFunctions.evaluate(toEvaluate);
	newComplexList = value;
	for (int j = coordinates.length; j > 0; j--) {
	    toEvaluate = complexList;

	    for (int i = 0; i < j; i++) {

		if (i == j - 1) {
		    toEvaluate.replaceElement(coordinates[i], newComplexList);
		    newComplexList = toEvaluate;
		} else {
		    toEvaluate = toEvaluate.getElement(coordinates[i]);
		}
	    }

	}
	return newComplexList;

    }

    private LispAtom evaluateElementaryComplexList(LispList list) {
	int[] firstSimpleListCoordinates = transformCoordinates(findFirstSimpleSublist(list));
	LispAtom value;
	while (firstSimpleListCoordinates[0] != -1) {
	    list = evaluateFirstSimpleSublist(list, firstSimpleListCoordinates);
	    firstSimpleListCoordinates = transformCoordinates(findFirstSimpleSublist(list));
	}
	value = (LispAtom) ElementaryFunctions.evaluate(list);
	return value;
    }

    private int[] transformCoordinates(Object[] coordinatePile) {
	int[] coordinates = new int[coordinatePile.length];
	for (int i = 0; i < coordinatePile.length; i++) {
	    coordinates[i] = (Integer) coordinatePile[i];
	}
	return coordinates;
    }

}
